using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Phoenix.Diagnostics;
using Phoenix.Exceptions;
using Phoenix.Utilities;
using TraceLevel = Phoenix.Diagnostics.TraceLevel;

namespace Phoenix.Framework
{
    public class DefaultControllerLocator : IControllerLocator
    {
        private readonly List<Assembly> scannedAssemblies;
        private readonly List<Assembly> assembliesToScan;
        private volatile Dictionary<string, List<Type>> controllers;
        private readonly object controllerLookupLock = new object();
        private readonly Type controllerType;

        public DefaultControllerLocator(params Assembly[] additionalAssemblies)
        {
            scannedAssemblies = new List<Assembly>();
            assembliesToScan = new List<Assembly>(additionalAssemblies);
            controllerType = typeof(Controller);
        }

        public Dictionary<string, List<Type>> Controllers
        {
            get
            {
                if (controllers == null)
                    lock (controllerLookupLock)
                        if (controllers == null)
                            using (TraceScope.Create(this, TraceLevel.Debug, () => "Scanning assemblies for controllers and building lookup"))
                            {
                                controllers = new Dictionary<string, List<Type>>();
                                foreach (var assembly in assembliesToScan)
                                {
                                    ScanAssembly(assembly);
                                }
                                assembliesToScan.Clear();
                            }

                return controllers;
            }
        }

        protected Dictionary<string, List<Type>> FindControllers(IEnumerable<Type> typesToScan)
        {
            var selectMany = typesToScan
                .Where(t => t.IsClass && t.Name.EndsWith("Controller") && controllerType.IsAssignableFrom(t))
                .SelectMany(t => new[]
                                     {
                                         new
                                             {
                                                 Name = t.Name.Replace("Controller", string.Empty),
                                                 Type = t
                                             },
                                         new
                                             {
                                                 t.Name,
                                                 Type = t
                                             }
                                     })
                                     .GroupBy(o => o.Name);
            return selectMany
                .ToDictionary(k => k.Key, k => k.Select(o => o.Type).ToList());
        }

        public void AddAssemblyToScan(Assembly assembly)
        {
            lock (controllerLookupLock)
            {
                if (controllers == null)
                {
                    assembliesToScan.Add(assembly);
                    return;
                }

                ScanAssembly(assembly);
            }
        }

        void ScanAssembly(Assembly assembly)
        {
            if (scannedAssemblies.Contains(assembly)) return;
            foreach (var controller in FindControllers(assembly.GetTypes()))
            {
                if (controllers.ContainsKey(controller.Key))
                    controllers[controller.Key].AddRange(controller.Value);
                else
                    controllers.Add(controller.Key, controller.Value);
            }
            scannedAssemblies.Add(assembly);
        }

        public IEnumerable<Type> GetControllersInAssembly(Assembly assembly)
        {
            return FindControllers(assembly.GetTypes()).Values.SelectMany(o => o).ToArray();
        }

        public Type Locate(ControllerAction controllerAction)
        {
            if (!Controllers.ContainsKey(controllerAction.Controller))
                throw new ControllerNotFoundException(controllerAction.Controller);

            var foundControllers = Controllers[controllerAction.Controller];
            Type locatedController;
            if (EvaluateControllers(controllerAction, foundControllers, out locatedController))
                return locatedController;

            if (string.IsNullOrEmpty(controllerAction.Assembly))
                throw new AmbiguousControllerNameException(controllerAction.Controller, foundControllers);

            //Try and assembly qualify
            foundControllers = foundControllers
                .Where(o => o.Assembly.GetAssemblyName() == controllerAction.Assembly)
                .ToList();

            if (foundControllers.Count > 1)
                throw new AmbiguousControllerNameException(controllerAction.Controller, foundControllers);

            EvaluateControllers(controllerAction, foundControllers, out locatedController);
            Debug.Assert(locatedController != null, "locatedController != null");
            return locatedController;
        }

        static bool EvaluateControllers(ControllerAction controllerAction, IList<Type> foundControllers, out Type locatedController)
        {
            if (foundControllers.Count == 0)
                throw new ControllerNotFoundException(controllerAction.Controller);

            if (foundControllers.Count == 1)
            {
                locatedController = foundControllers[0];
                return true;
            }

            locatedController = null;
            return false;
        }

        static string CreateExceptionMessage(ControllerAction controllerAction)
        {
            return string.Format(@"Failed to invoke the default controller action {0}. 
Either create that controller action, or configure the default controller action to another action.

Example controller:
public class {1}Controller : Phoenix.Controller
{{
    public ActionResult {2}()
    {{
        return Page(new {2}ViewModel());
    }}
}}", controllerAction, controllerAction.Controller, controllerAction.Action);
        }
    }
}